{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6b82288e",
   "metadata": {},
   "source": [
    "# LangChain의 RAG 파헤치기\n",
    "\n",
    "![rag-1.png](./assets/rag-1.png)\n",
    "\n",
    "![rag-2.png](./assets/rag-2.png)\n",
    "\n",
    "### 1. 질문 처리\n",
    "\n",
    "질문 처리 단계에서는 사용자의 질문을 받아 이를 처리하고, 관련 데이터를 찾는 작업이 이루어집니다. 이를 위해 다음과 같은 구성 요소들이 필요합니다:\n",
    "\n",
    "- **데이터 소스 연결**: 질문에 대한 답변을 찾기 위해 다양한 텍스트 데이터 소스에 연결해야 합니다. LangChain은 다양한 데이터 소스와의 연결을 간편하게 설정할 수 있도록 돕습니다.\n",
    "- **데이터 인덱싱 및 검색**: 데이터 소스에서 관련 정보를 효율적으로 찾기 위해, 데이터는 인덱싱되어야 합니다. LangChain은 인덱싱 과정을 자동화하고, 사용자의 질문과 관련된 데이터를 검색하는 데 필요한 도구를 제공합니다.\n",
    "\n",
    "### 2. 답변 생성\n",
    "\n",
    "관련 데이터를 찾은 후에는 이를 기반으로 사용자의 질문에 답변을 생성해야 합니다. 이 단계에서는 다음 구성 요소가 중요합니다:\n",
    "\n",
    "- **답변 생성 모델**: LangChain은 고급 자연어 처리(NLP) 모델을 사용하여 검색된 데이터로부터 답변을 생성할 수 있는 기능을 제공합니다. 이러한 모델은 사용자의 질문과 검색된 데이터를 입력으로 받아, 적절한 답변을 생성합니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "66d6f1b5",
   "metadata": {},
   "source": [
    "## 아키텍처\n",
    "\n",
    "우리는 [Q&A 소개](https://python.langchain.comhttps://python.langchain.com/docs/use_cases/question_answering/)에서 개요한 대로 전형적인 RAG 애플리케이션을 만들 것입니다. 이것은 두 가지 주요 구성 요소를 가지고 있습니다:\n",
    "\n",
    "- **인덱싱**: 소스에서 데이터를 수집하고 인덱싱하는 파이프라인입니다. _이 작업은 보통 오프라인에서 발생합니다._\n",
    "\n",
    "- **검색 및 생성**: 실제 RAG 체인으로, 사용자 쿼리를 실행 시간에 받아 인덱스에서 관련 데이터를 검색한 다음, 그 데이터를 모델에 전달합니다.\n",
    "\n",
    "RAW 데이터에서 답변을 받기까지의 전체 순서는 다음과 같습니다.\n",
    "\n",
    "### 인덱싱\n",
    "\n",
    "![](https://python.langchain.com/assets/images/rag_indexing-8160f90a90a33253d0154659cf7d453f.png)\n",
    "\n",
    "1. **로드**: 먼저 데이터를 로드해야 합니다. 이를 위해 [DocumentLoaders](https://python.langchain.com/docs/modules/data_connection/document_loaders/)를 사용할 것입니다.\n",
    "2. **분할**: [Text splitters](https://python.langchain.com/docs/modules/data_connection/document_transformers/)는 큰 `Documents`를 더 작은 청크로 나눕니다. 이는 데이터를 인덱싱하고 모델에 전달하는 데 유용하며, 큰 청크는 검색하기 어렵고 모델의 유한한 컨텍스트 창에 맞지 않습니다.\n",
    "3. **저장**: 나중에 검색할 수 있도록 분할을 저장하고 인덱싱할 장소가 필요합니다. 이는 종종 [VectorStore](https://python.langchain.com/docs/modules/data_connection/vectorstores/)와 [Embeddings](https://python.langchain.com/docs/modules/data_connection/text_embedding/) 모델을 사용하여 수행됩니다.\n",
    "\n",
    "### 검색 및 생성\n",
    "\n",
    "![](https://python.langchain.com/assets/images/rag_retrieval_generation-1046a4668d6bb08786ef73c56d4f228a.png)\n",
    "\n",
    "1. **검색**: 사용자 입력이 주어지면 [Retriever](https://python.langchain.com/docs/modules/data_connection/retrievers/)를 사용하여 저장소에서 관련 분할을 검색합니다.\n",
    "2. **생성**: [ChatModel](https://python.langchain.com/docs/modules/model_io/chat/) / [LLM](https://python.langchain.com/docs/modules/model_io/llms/)은 질문과 검색된 데이터를 포함한 프롬프트를 사용하여 답변을 생성합니다\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1cf05522",
   "metadata": {},
   "source": [
    "## 실습에 활용한 문서\n",
    "\n",
    "소프트웨어정책연구소(SPRi) - 2023년 12월호\n",
    "\n",
    "- 저자: 유재흥(AI정책연구실 책임연구원), 이지수(AI정책연구실 위촉연구원)\n",
    "- 링크: https://spri.kr/posts/view/23669\n",
    "- 파일명: `SPRI_AI_Brief_2023년12월호_F.pdf`\n",
    "\n",
    "_실습을 위해 다운로드 받은 파일을 `data` 폴더로 복사해 주시기 바랍니다_\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "01c423a8",
   "metadata": {},
   "source": [
    "## 환경설정\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a224fd32",
   "metadata": {},
   "source": [
    "API KEY 를 설정합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "418ab505",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0024d0c5",
   "metadata": {},
   "source": [
    "LangChain으로 구축한 애플리케이션은 여러 단계에 걸쳐 LLM 호출을 여러 번 사용하게 됩니다. 이러한 애플리케이션이 점점 더 복잡해짐에 따라, 체인이나 에이전트 내부에서 정확히 무슨 일이 일어나고 있는지 조사할 수 있는 능력이 매우 중요해집니다. 이를 위한 최선의 방법은 [LangSmith](https://smith.langchain.com)를 사용하는 것입니다.\n",
    "\n",
    "LangSmith가 필수는 아니지만, 유용합니다. LangSmith를 사용하고 싶다면, 위의 링크에서 가입한 후, 로깅 추적을 시작하기 위해 환경 변수를 설정해야 합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3edbbf89",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH12-RAG\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b0d050a",
   "metadata": {},
   "source": [
    "## 모듈별로 자세히 살펴보기\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f3d1b0fc",
   "metadata": {},
   "outputs": [],
   "source": [
    "import bs4\n",
    "from langchain import hub\n",
    "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
    "from langchain_community.document_loaders import WebBaseLoader\n",
    "from langchain_community.vectorstores import Chroma, FAISS\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "from langchain_core.runnables import RunnablePassthrough\n",
    "from langchain_openai import ChatOpenAI, OpenAIEmbeddings"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48e783c4",
   "metadata": {},
   "source": [
    "아래는 [](https://teddylee777.github.io/langchain/rag-naver-news-qa/) 에서 다뤘던 기본적인 RAG 모델을 사용하는 예제입니다.\n",
    "\n",
    "여기서 각 단계별로 다양한 옵션을 설정하거나 새로운 기법을 적용할 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "377894c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 1: 문서 로드(Load Documents)\n",
    "# 뉴스기사 내용을 로드하고, 청크로 나누고, 인덱싱합니다.\n",
    "url = \"https://n.news.naver.com/article/437/0000378416\"\n",
    "loader = WebBaseLoader(\n",
    "    web_paths=(url,),\n",
    "    bs_kwargs=dict(\n",
    "        parse_only=bs4.SoupStrainer(\n",
    "            \"div\",\n",
    "            attrs={\"class\": [\"newsct_article _article_body\", \"media_end_head_title\"]},\n",
    "        )\n",
    "    ),\n",
    ")\n",
    "docs = loader.load()\n",
    "\n",
    "\n",
    "# 단계 2: 문서 분할(Split Documents)\n",
    "text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=50)\n",
    "\n",
    "splits = text_splitter.split_documents(docs)\n",
    "\n",
    "# 단계 3: 임베딩 & 벡터스토어 생성(Create Vectorstore)\n",
    "# 벡터스토어를 생성합니다.\n",
    "vectorstore = FAISS.from_documents(documents=splits, embedding=OpenAIEmbeddings())\n",
    "\n",
    "# 단계 4: 검색(Search)\n",
    "# 뉴스에 포함되어 있는 정보를 검색하고 생성합니다.\n",
    "retriever = vectorstore.as_retriever()\n",
    "\n",
    "# 단계 5: 프롬프트 생성(Create Prompt)\n",
    "# 프롬프트를 생성합니다.\n",
    "prompt = hub.pull(\"rlm/rag-prompt\")\n",
    "\n",
    "# 단계 6: 언어모델 생성(Create LLM)\n",
    "# 모델(LLM) 을 생성합니다.\n",
    "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
    "\n",
    "\n",
    "def format_docs(docs):\n",
    "    # 검색한 문서 결과를 하나의 문단으로 합쳐줍니다.\n",
    "    return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
    "\n",
    "\n",
    "# 단계 7: 체인 생성(Create Chain)\n",
    "rag_chain = (\n",
    "    {\"context\": retriever | format_docs, \"question\": RunnablePassthrough()}\n",
    "    | prompt\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "# 단계 8: 체인 실행(Run Chain)\n",
    "# 문서에 대한 질의를 입력하고, 답변을 출력합니다.\n",
    "question = \"부영그룹의 출산 장려 정책에 대해 설명해주세요\"\n",
    "response = rag_chain.invoke(question)\n",
    "\n",
    "# 결과 출력\n",
    "print(f\"URL: {url}\")\n",
    "print(f\"문서의 수: {len(docs)}\")\n",
    "print(\"===\" * 20)\n",
    "print(f\"[HUMAN]\\n{question}\\n\")\n",
    "print(f\"[AI]\\n{response}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31b686d9",
   "metadata": {},
   "source": [
    "## 단계 1: 문서 로드(Load Documents)\n",
    "\n",
    "- [공식문서 링크 - Document loaders](https://python.langchain.com/docs/modules/data_connection/document_loaders/)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "595217a5",
   "metadata": {},
   "source": [
    "### 웹페이지\n",
    "\n",
    "`WebBaseLoader`는 지정된 웹 페이지에서 필요한 부분만을 파싱하기 위해 `bs4.SoupStrainer`를 사용합니다.\n",
    "\n",
    "[참고]\n",
    "\n",
    "- `bs4.SoupStrainer` 는 편리하게 웹에서 원하는 요소를 가져올 수 있도록 해줍니다.\n",
    "\n",
    "(예시)\n",
    "\n",
    "```python\n",
    "bs4.SoupStrainer(\n",
    "    \"div\",\n",
    "    attrs={\"class\": [\"newsct_article _article_body\", \"media_end_head_title\"]}, # 클래스 명을 입력\n",
    ")\n",
    "\n",
    "bs4.SoupStrainer(\n",
    "    \"article\",\n",
    "    attrs={\"id\": [\"dic_area\"]}, # 클래스 명을 입력\n",
    ")\n",
    "```\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8a040ec3",
   "metadata": {},
   "source": [
    "아래의 BBC 뉴스 기사입니다. 영문으로 작성된 기사로 시험해 보고 싶다면, 아래의 주석을 해제하고 실행해 보세요.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fc118579",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 뉴스기사의 내용을 로드하고, 청크로 나누고, 인덱싱합니다.\n",
    "loader = WebBaseLoader(\n",
    "    web_paths=(\"https://www.bbc.com/news/business-68092814\",),\n",
    "    bs_kwargs=dict(\n",
    "        parse_only=bs4.SoupStrainer(\n",
    "            \"main\",\n",
    "            attrs={\"id\": [\"main-content\"]},\n",
    "        )\n",
    "    ),\n",
    ")\n",
    "docs = loader.load()\n",
    "print(f\"문서의 수: {len(docs)}\")\n",
    "docs[0].page_content[:500]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f43be1cf",
   "metadata": {},
   "source": [
    "### PDF\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b14f827c",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.document_loaders import PyPDFLoader\n",
    "\n",
    "# PDF 파일 로드. 파일의 경로 입력\n",
    "loader = PyPDFLoader(\"data/SPRI_AI_Brief_2023년12월호_F.pdf\")\n",
    "\n",
    "# 페이지 별 문서 로드\n",
    "docs = loader.load()\n",
    "print(f\"문서의 수: {len(docs)}\")\n",
    "\n",
    "# 10번째 페이지의 내용 출력\n",
    "print(f\"\\n[페이지내용]\\n{docs[10].page_content[:500]}\")\n",
    "print(f\"\\n[metadata]\\n{docs[10].metadata}\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a45216d9",
   "metadata": {},
   "source": [
    "### CSV\n",
    "\n",
    "CSV 는 페이지 번호 대신 행번호로 데이터를 조회합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "77419a2a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders.csv_loader import CSVLoader\n",
    "\n",
    "# CSV 파일 로드\n",
    "loader = CSVLoader(file_path=\"data/titanic.csv\")\n",
    "docs = loader.load()\n",
    "print(f\"문서의 수: {len(docs)}\")\n",
    "\n",
    "# 10번째 페이지의 내용 출력\n",
    "print(f\"\\n[페이지내용]\\n{docs[10].page_content[:500]}\")\n",
    "print(f\"\\n[metadata]\\n{docs[10].metadata}\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4866c8ea",
   "metadata": {},
   "source": [
    "### TXT 파일\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e5c00113",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import TextLoader\n",
    "\n",
    "loader = TextLoader(\"data/appendix-keywords.txt\")\n",
    "docs = loader.load()\n",
    "print(f\"문서의 수: {len(docs)}\")\n",
    "\n",
    "# 10번째 페이지의 내용 출력\n",
    "print(f\"\\n[페이지내용]\\n{docs[0].page_content[:500]}\")\n",
    "print(f\"\\n[metadata]\\n{docs[0].metadata}\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7567b4e0",
   "metadata": {},
   "source": [
    "### 폴더 내의 모든 파일 로드\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "133bf8dd",
   "metadata": {},
   "source": [
    "아래는 폴더 내 모든 `.txt` 파일을 로드하는 예시입니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fecb15de",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import DirectoryLoader\n",
    "\n",
    "loader = DirectoryLoader(\".\", glob=\"data/*.txt\", show_progress=True)\n",
    "docs = loader.load()\n",
    "\n",
    "print(f\"문서의 수: {len(docs)}\")\n",
    "\n",
    "# 10번째 페이지의 내용 출력\n",
    "print(f\"\\n[페이지내용]\\n{docs[0].page_content[:500]}\")\n",
    "print(f\"\\n[metadata]\\n{docs[0].metadata}\\n\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6b9cbb02",
   "metadata": {},
   "source": [
    "다음은 폴더내 모든 `.pdf` 파일을 로드하는 예제입니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0cc40246",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import DirectoryLoader\n",
    "\n",
    "loader = DirectoryLoader(\".\", glob=\"data/*.pdf\")\n",
    "docs = loader.load()\n",
    "\n",
    "print(f\"문서의 수: {len(docs)}\\n\")\n",
    "print(\"[메타데이터]\\n\")\n",
    "print(docs[0].metadata)\n",
    "print(\"\\n========= [앞부분] 미리보기 =========\\n\")\n",
    "print(docs[0].page_content[2500:3000])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9d3065d6",
   "metadata": {},
   "source": [
    "### Python\n",
    "\n",
    "다음은 `.py` 파일을 로드하는 예제입니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "36678ef2",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.document_loaders import PythonLoader\n",
    "\n",
    "loader = DirectoryLoader(\".\", glob=\"**/*.py\", loader_cls=PythonLoader)\n",
    "docs = loader.load()\n",
    "\n",
    "print(f\"문서의 수: {len(docs)}\\n\")\n",
    "print(\"[메타데이터]\\n\")\n",
    "print(docs[0].metadata)\n",
    "print(\"\\n========= [앞부분] 미리보기 =========\\n\")\n",
    "print(docs[0].page_content[:500])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "505bd21b",
   "metadata": {},
   "source": [
    "---\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b0321a46",
   "metadata": {},
   "source": [
    "## 단계 2: 문서 분할(Split Documents)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "47ad4770",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 뉴스기사의 내용을 로드하고, 청크로 나누고, 인덱싱합니다.\n",
    "loader = WebBaseLoader(\n",
    "    web_paths=(\"https://www.bbc.com/news/business-68092814\",),\n",
    "    bs_kwargs=dict(\n",
    "        parse_only=bs4.SoupStrainer(\n",
    "            \"main\",\n",
    "            attrs={\"id\": [\"main-content\"]},\n",
    "        )\n",
    "    ),\n",
    ")\n",
    "docs = loader.load()\n",
    "print(f\"문서의 수: {len(docs)}\")\n",
    "docs[0].page_content[:500]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "62dc11cb",
   "metadata": {},
   "source": [
    "### CharacterTextSplitter\n",
    "\n",
    "이것은 가장 간단한 방법입니다. 이 방법은 문자를 기준으로 분할합니다(기본값은 \"\\n\\n\") 그리고 청크의 길이를 문자의 수로 측정합니다.\n",
    "\n",
    "1. 텍스트가 어떻게 분할되는지: 단일 문자 단위\n",
    "2. 청크 크기가 어떻게 측정되는지: `len` of characters.\n",
    "\n",
    "시각화 예제: https://chunkviz.up.railway.app/\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14d4a799",
   "metadata": {},
   "source": [
    "`CharacterTextSplitter` 클래스는 텍스트를 특정 크기의 청크로 분할하는 기능을 제공합니다.\n",
    "\n",
    "- `separator` 매개변수는 청크를 구분하는 데 사용되는 문자열을 지정하며, 여기서는 두 개의 개행 문자(`\"\\n\\n\"`)를 사용합니다\n",
    "- `chunk_size`는 각 청크의 최대 길이를 결정합니다\n",
    "- `chunk_overlap`은 인접한 청크 간에 겹치는 문자의 수를 지정합니다.\n",
    "- `length_function`은 청크의 길이를 계산하는 데 사용되는 함수를 결정하며, 기본적으로 문자열의 길이를 반환하는 `len` 함수가 사용됩니다.\n",
    "- `is_separator_regex`는 `separator`가 정규 표현식으로 해석될지 여부를 결정하는 불리언 값입니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ade83078",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.text_splitter import CharacterTextSplitter\n",
    "\n",
    "text_splitter = CharacterTextSplitter(\n",
    "    separator=\"\\n\\n\",\n",
    "    chunk_size=100,\n",
    "    chunk_overlap=10,\n",
    "    length_function=len,\n",
    "    is_separator_regex=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea7d7e0d",
   "metadata": {},
   "source": [
    "이 함수는 `text_splitter` 객체의 `create_documents` 메소드를 사용하여 주어진 텍스트(`state_of_the_union`)를 여러 문서로 분할하고, 그 결과를 `texts` 변수에 저장합니다. 이후 `texts`의 첫 번째 문서를 출력합니다. 이 과정은 텍스트 데이터를 처리하고 분석하기 위한 초기 단계로 볼 수 있으며, 특히 큰 텍스트 데이터를 관리 가능한 크기의 단위로 나누는 데 유용합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "472bc3c3",
   "metadata": {},
   "outputs": [],
   "source": [
    "# chain of density 논문의 일부 내용을 불러옵니다\n",
    "with open(\"data/chain-of-density.txt\", \"r\") as f:\n",
    "    text = f.read()[:500]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "70e11f5b",
   "metadata": {},
   "outputs": [],
   "source": [
    "text_splitter = CharacterTextSplitter(\n",
    "    chunk_size=100, chunk_overlap=10, separator=\"\\n\\n\"\n",
    ")\n",
    "text_splitter.split_text(text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "24506d98",
   "metadata": {},
   "outputs": [],
   "source": [
    "text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=10, separator=\"\\n\")\n",
    "text_splitter.split_text(text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "57da16a7",
   "metadata": {},
   "outputs": [],
   "source": [
    "text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=10, separator=\" \")\n",
    "text_splitter.split_text(text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9c743226",
   "metadata": {},
   "outputs": [],
   "source": [
    "text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0, separator=\" \")\n",
    "text_splitter.split_text(text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2b5b02d4",
   "metadata": {},
   "outputs": [],
   "source": [
    "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100, separator=\" \")\n",
    "# text 파일을 청크로 나누어줍니다.\n",
    "text_splitter.split_text(text)\n",
    "\n",
    "# document를 청크로 나누어줍니다.\n",
    "split_docs = text_splitter.split_documents(docs)\n",
    "len(split_docs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e2f39fc8",
   "metadata": {},
   "outputs": [],
   "source": [
    "split_docs[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e376c2cd",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 뉴스기사의 내용을 로드하고, 청크로 나누고, 인덱싱합니다.\n",
    "loader = WebBaseLoader(\n",
    "    web_paths=(\"https://www.bbc.com/news/business-68092814\",),\n",
    "    bs_kwargs=dict(\n",
    "        parse_only=bs4.SoupStrainer(\n",
    "            \"main\",\n",
    "            attrs={\"id\": [\"main-content\"]},\n",
    "        )\n",
    "    ),\n",
    ")\n",
    "\n",
    "# splitter 를 정의합니다.\n",
    "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100, separator=\" \")\n",
    "\n",
    "# 문서를 로드시 바로 분할까지 수행합니다.\n",
    "split_docs = loader.load_and_split(text_splitter=text_splitter)\n",
    "print(f\"문서의 수: {len(docs)}\")\n",
    "docs[0].page_content[:500]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dc3662cd",
   "metadata": {},
   "source": [
    "### RecursiveTextSplitter\n",
    "\n",
    "이 텍스트 분할기는 일반 텍스트에 권장되는 텍스트 분할기입니다.\n",
    "\n",
    "1. 텍스트가 어떻게 분할 규칙: list of `separators`\n",
    "2. 청크 크기가 어떻게 측정되는가: `len` of characters\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f33726f8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# langchain 패키지에서 RecursiveCharacterTextSplitter 클래스를 가져옵니다.\n",
    "from langchain.text_splitter import RecursiveCharacterTextSplitter"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bd8a2dad",
   "metadata": {},
   "source": [
    "`RecursiveCharacterTextSplitter` 클래스는 텍스트를 재귀적으로 분할하는 기능을 제공합니다. 이 클래스는 `chunk_size`로 분할할 청크의 크기, `chunk_overlap`으로 인접 청크 간의 겹침 크기, `length_function`으로 청크의 길이를 계산하는 함수, 그리고 `is_separator_regex`로 구분자가 정규 표현식인지 여부를 지정하는 매개변수를 받습니다. 예시에서는 청크 크기를 100, 겹침 크기를 20으로 설정하고, 길이 계산 함수로 `len`을 사용하며, 구분자가 정규 표현식이 아님을 나타내기 위해 `is_separator_regex`를 `False`로 설정합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5fc0587c",
   "metadata": {},
   "outputs": [],
   "source": [
    "recursive_text_splitter = RecursiveCharacterTextSplitter(\n",
    "    # 정말 작은 청크 크기를 설정합니다.\n",
    "    chunk_size=100,\n",
    "    chunk_overlap=10,\n",
    "    length_function=len,\n",
    "    is_separator_regex=False,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6a8cf631",
   "metadata": {},
   "outputs": [],
   "source": [
    "# chain of density 논문의 일부 내용을 불러옵니다\n",
    "with open(\"data/chain-of-density.txt\", \"r\") as f:\n",
    "    text = f.read()[:500]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "26f2bb98",
   "metadata": {},
   "outputs": [],
   "source": [
    "character_text_splitter = CharacterTextSplitter(\n",
    "    chunk_size=100, chunk_overlap=10, separator=\" \"\n",
    ")\n",
    "for sent in character_text_splitter.split_text(text):\n",
    "    print(sent)\n",
    "print(\"===\" * 20)\n",
    "recursive_text_splitter = RecursiveCharacterTextSplitter(\n",
    "    chunk_size=100, chunk_overlap=10\n",
    ")\n",
    "for sent in recursive_text_splitter.split_text(text):\n",
    "    print(sent)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "264cf4ae",
   "metadata": {},
   "source": [
    "- 지정한 separators 리스트를 순차적으로 시도하며 주어진 문서를 분할합니다.\n",
    "- 청크가 충분히 작아질 때까지 순서대로 분할을 시도합니다. 기본 목록은 [\"\\n\\n\", \"\\n\", \" \", \"\"]입니다.\n",
    "- 이는 일반적으로 의미적으로 가장 연관성이 강한 텍스트 조각인 것처럼 보이는 모든 단락(그리고 문장, 단어)을 가능한 한 길게 유지하려는 효과가 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "393ec4f4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# recursive_text_splitter 에 기본 지정된 separators 를 확인합니다.\n",
    "recursive_text_splitter._separators"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e49d975",
   "metadata": {},
   "source": [
    "### Semantic Similarity\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05729bc0",
   "metadata": {},
   "source": [
    "의미적 유사성을 기준으로 텍스트를 분할합니다.\n",
    "\n",
    "출처: [Greg Kamradt’s Notebook](https://github.com/FullStackRetrieval-com/RetrievalTutorials/blob/main/5_Levels_Of_Text_Splitting.ipynb)\n",
    "\n",
    "높은 수준(high level)에서 문장으로 분할한 다음 3개 문장으로 그룹화한 다음 임베딩 공간에서 유사한 문장을 병합하는 방식입니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "206ff657",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 최신 버전으로 업데이트합니다.\n",
    "# !pip install -U langchain langchain_experimental -q"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dccff243",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_experimental.text_splitter import SemanticChunker\n",
    "from langchain_openai.embeddings import OpenAIEmbeddings\n",
    "\n",
    "# SemanticChunker 를 생성합니다.\n",
    "semantic_text_splitter = SemanticChunker(OpenAIEmbeddings(), add_start_index=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a27b2d73",
   "metadata": {},
   "outputs": [],
   "source": [
    "# chain of density 논문의 일부 내용을 불러옵니다\n",
    "with open(\"data/chain-of-density.txt\", \"r\") as f:\n",
    "    text = f.read()\n",
    "\n",
    "for sent in semantic_text_splitter.split_text(text):\n",
    "    print(sent)\n",
    "    print(\"===\" * 20)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f04f3467",
   "metadata": {},
   "source": [
    "## 3 단계: 임베딩\n",
    "\n",
    "참고: https://python.langchain.com/docs/integrations/text_embedding\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9affbaf8",
   "metadata": {},
   "source": [
    "### 유료 과금 임베딩(OpenAI)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ccc62a14",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.vectorstores import FAISS\n",
    "from langchain_openai.embeddings import OpenAIEmbeddings\n",
    "\n",
    "# 단계 3: 임베딩 & 벡터스토어 생성(Create Vectorstore)\n",
    "# 벡터스토어를 생성합니다.\n",
    "vectorstore = FAISS.from_documents(documents=splits, embedding=OpenAIEmbeddings())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7f65e243",
   "metadata": {},
   "source": [
    "다음은 `OpenAI` 의 지원되는 Embedding 모델들의 목록입니다.\n",
    "\n",
    "- 기본 값은 `text-embeding-ada-002` 입니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "97c021b1",
   "metadata": {},
   "source": [
    "| MODEL                  | ROUGH PAGES PER DOLLAR | EXAMPLE PERFORMANCE ON MTEB EVAL |\n",
    "| ---------------------- | ---------------------- | -------------------------------- |\n",
    "| text-embedding-3-small | 62,500                 | 62.3%                            |\n",
    "| text-embedding-3-large | 9,615                  | 64.6%                            |\n",
    "| text-embedding-ada-002 | 12,500                 | 61.0%                            |\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8d07fbf1",
   "metadata": {},
   "outputs": [],
   "source": [
    "vectorstore = FAISS.from_documents(\n",
    "    documents=splits, embedding=OpenAIEmbeddings(model=\"text-embedding-3-small\")\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "241cf6e8",
   "metadata": {},
   "source": [
    "### 무료 Open Source 기반 임베딩\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8ca55cbf",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.embeddings import HuggingFaceBgeEmbeddings\n",
    "\n",
    "# 단계 3: 임베딩 & 벡터스토어 생성(Create Vectorstore)\n",
    "# 벡터스토어를 생성합니다.\n",
    "vectorstore = FAISS.from_documents(\n",
    "    documents=splits, embedding=HuggingFaceBgeEmbeddings()\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2964bff5",
   "metadata": {},
   "outputs": [],
   "source": [
    "# !pip install fastembed -q"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5d13003a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.embeddings.fastembed import FastEmbedEmbeddings\n",
    "\n",
    "vectorstore = FAISS.from_documents(documents=splits, embedding=FastEmbedEmbeddings())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd42083c",
   "metadata": {},
   "source": [
    "## 4단계: 벡터스토어 생성(Create Vectorstore)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9033e0af",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.vectorstores import FAISS\n",
    "\n",
    "# FAISS DB 적용\n",
    "vectorstore = FAISS.from_documents(documents=splits, embedding=OpenAIEmbeddings())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d0a99a27",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.vectorstores import Chroma\n",
    "\n",
    "# Chroma DB 적용\n",
    "vectorstore = Chroma.from_documents(documents=splits, embedding=OpenAIEmbeddings())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8150225",
   "metadata": {},
   "source": [
    "## 5단계: Retriever 생성\n",
    "\n",
    "리트리버는 구조화되지 않은 쿼리가 주어지면 문서를 반환하는 인터페이스입니다.\n",
    "\n",
    "리트리버는 문서를 저장할 필요 없이 문서를 반환(또는 검색)하기만 합니다.\n",
    "\n",
    "- [공식 도큐먼트](https://python.langchain.com/docs/modules/data_connection/retrievers/)\n",
    "\n",
    "생성된 VectorStore 에 `as_retriver()` 로 가져와서 **Retriever** 를 생성합니다.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "90798e85",
   "metadata": {},
   "source": [
    "### 유사도 기반 검색\n",
    "\n",
    "- 기본값은 코사인 유사도인 `similarity` 가 적용되어 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b2bd6d95",
   "metadata": {},
   "outputs": [],
   "source": [
    "query = \"회사의 저출생 정책이 뭐야?\"\n",
    "\n",
    "retriever = vectorstore.as_retriever(search_type=\"similarity\")\n",
    "search_result = retriever.get_relevant_documents(query)\n",
    "print(search_result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e107dea5",
   "metadata": {},
   "source": [
    "`similarity_score_threshold` 는 유사도 기반 검색에서 `score_threshold` 이상인 결과만 반환합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "067f05aa",
   "metadata": {},
   "outputs": [],
   "source": [
    "query = \"회사의 저출생 정책이 뭐야?\"\n",
    "\n",
    "retriever = vectorstore.as_retriever(\n",
    "    search_type=\"similarity_score_threshold\", search_kwargs={\"score_threshold\": 0.8}\n",
    ")\n",
    "search_result = retriever.get_relevant_documents(query)\n",
    "print(search_result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a72fee8",
   "metadata": {},
   "source": [
    "`maximum marginal search result` 를 사용하여 검색합니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "405337a9",
   "metadata": {},
   "outputs": [],
   "source": [
    "query = \"회사의 저출생 정책이 뭐야?\"\n",
    "\n",
    "retriever = vectorstore.as_retriever(search_type=\"mmr\", search_kwargs={\"k\": 2})\n",
    "search_result = retriever.get_relevant_documents(query)\n",
    "print(search_result)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e9a96b86",
   "metadata": {},
   "source": [
    "### 다양한 쿼리 생성\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3473f1af",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.retrievers.multi_query import MultiQueryRetriever\n",
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "query = \"회사의 저출생 정책이 뭐야?\"\n",
    "\n",
    "llm = ChatOpenAI(temperature=0, model=\"gpt-3.5-turbo\")\n",
    "\n",
    "retriever_from_llm = MultiQueryRetriever.from_llm(\n",
    "    retriever=vectorstore.as_retriever(), llm=llm\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e743451f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set logging for the queries\n",
    "import logging\n",
    "\n",
    "logging.basicConfig()\n",
    "logging.getLogger(\"langchain.retrievers.multi_query\").setLevel(logging.INFO)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cd0aed2a",
   "metadata": {},
   "outputs": [],
   "source": [
    "unique_docs = retriever_from_llm.get_relevant_documents(query=question)\n",
    "len(unique_docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c8185cf7",
   "metadata": {},
   "source": [
    "### Ensemble Retriever\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cd02b2b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.retrievers import BM25Retriever, EnsembleRetriever\n",
    "from langchain_community.vectorstores import FAISS\n",
    "from langchain_openai import OpenAIEmbeddings"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "52988fd9",
   "metadata": {},
   "outputs": [],
   "source": [
    "doc_list = [\n",
    "    \"난 오늘 많이 먹어서 배가 정말 부르다\",\n",
    "    \"떠나는 저 배가 오늘 마지막 배인가요?\",\n",
    "    \"내가 제일 좋아하는 과일들은 배, 사과, 키워, 수박 입니다.\",\n",
    "]\n",
    "\n",
    "# initialize the bm25 retriever and faiss retriever\n",
    "bm25_retriever = BM25Retriever.from_texts(doc_list)\n",
    "bm25_retriever.k = 2\n",
    "\n",
    "faiss_vectorstore = FAISS.from_texts(doc_list, OpenAIEmbeddings())\n",
    "faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={\"k\": 2})\n",
    "\n",
    "# initialize the ensemble retriever\n",
    "ensemble_retriever = EnsembleRetriever(\n",
    "    retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6658f807",
   "metadata": {},
   "outputs": [],
   "source": [
    "def pretty_print(docs):\n",
    "    for i, doc in enumerate(docs):\n",
    "        print(f\"[{i+1}] {doc.page_content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f046a5bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_query = \"나 요즘 배에 정말 살이 많이 쪘어...\"\n",
    "print(f\"[Query]\\n{sample_query}\\n\")\n",
    "relevant_docs = bm25_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[BM25 Retriever]\")\n",
    "pretty_print(relevant_docs)\n",
    "print(\"===\" * 20)\n",
    "relevant_docs = faiss_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[FAISS Retriever]\")\n",
    "pretty_print(relevant_docs)\n",
    "print(\"===\" * 20)\n",
    "relevant_docs = ensemble_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[Ensemble Retriever]\")\n",
    "pretty_print(relevant_docs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "99d87210",
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_query = \"바다 위에 떠다니는 배들이 많다\"\n",
    "print(f\"[Query]\\n{sample_query}\\n\")\n",
    "relevant_docs = bm25_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[BM25 Retriever]\")\n",
    "pretty_print(relevant_docs)\n",
    "print(\"===\" * 20)\n",
    "relevant_docs = faiss_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[FAISS Retriever]\")\n",
    "pretty_print(relevant_docs)\n",
    "print(\"===\" * 20)\n",
    "relevant_docs = ensemble_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[Ensemble Retriever]\")\n",
    "pretty_print(relevant_docs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "60a3345b",
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_query = \"ships\"\n",
    "print(f\"[Query]\\n{sample_query}\\n\")\n",
    "relevant_docs = bm25_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[BM25 Retriever]\")\n",
    "pretty_print(relevant_docs)\n",
    "print(\"===\" * 20)\n",
    "relevant_docs = faiss_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[FAISS Retriever]\")\n",
    "pretty_print(relevant_docs)\n",
    "print(\"===\" * 20)\n",
    "relevant_docs = ensemble_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[Ensemble Retriever]\")\n",
    "pretty_print(relevant_docs)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c2210c1a",
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_query = \"pear\"\n",
    "print(f\"[Query]\\n{sample_query}\\n\")\n",
    "relevant_docs = bm25_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[BM25 Retriever]\")\n",
    "pretty_print(relevant_docs)\n",
    "print(\"===\" * 20)\n",
    "relevant_docs = faiss_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[FAISS Retriever]\")\n",
    "pretty_print(relevant_docs)\n",
    "print(\"===\" * 20)\n",
    "relevant_docs = ensemble_retriever.get_relevant_documents(sample_query)\n",
    "print(\"[Ensemble Retriever]\")\n",
    "pretty_print(relevant_docs)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56e9d842",
   "metadata": {},
   "source": [
    "## 6단계: 프롬프트 생성(Create Prompt)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b5d94fcc",
   "metadata": {},
   "source": [
    "프롬프트 엔지니어링은 주어진 데이터(`context`)를 토대로 우리가 원하는 결과를 도출할 때 중요한 역할을 합니다.\n",
    "\n",
    "[TIP1]\n",
    "\n",
    "1. 만약, `retriever` 에서 도출한 결과에서 중요한 정보가 누락된다면 `retriever` 의 로직을 수정해야 합니다.\n",
    "2. 만약, `retriever` 에서 도출한 결과가 많은 정보를 포함하고 있지만, `llm` 이 그 중에서 중요한 정보를 찾지 못한거나 원하는 형태로 출력하지 않는다면 프롬프트를 수정해야 합니다.\n",
    "\n",
    "[TIP2]\n",
    "\n",
    "1. LangSmith 의 **hub** 에는 검증된 프롬프트가 많이 업로드 되어 있습니다.\n",
    "2. 검증된 프롬프트를 활용하거나 약간 수정한다면 비용과 시간을 절약할 수 있습니다.\n",
    "\n",
    "- https://smith.langchain.com/hub/search?q=rag\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20486a6f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain import hub"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "61e05003",
   "metadata": {},
   "outputs": [],
   "source": [
    "prompt = hub.pull(\"rlm/rag-prompt\")\n",
    "prompt"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc0d8714",
   "metadata": {},
   "source": [
    "## 7단계: 언어모델 생성(Create LLM)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37f99381",
   "metadata": {},
   "source": [
    "OpenAI 모델 중 하나를 선택합니다.\n",
    "\n",
    "- `gpt-3.5-turbo` : OpenAI의 GPT-3.5-turbo 모델\n",
    "- `gpt-4-turbo-preview` : OpenAI의 GPT-4-turbo-preview 모델\n",
    "\n",
    "자세한 비용 체계는 [OpenAI API 모델 리스트 / 요금표](https://teddylee777.github.io/openai/openai-models/)에서 확인할 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "85dcaa69",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "model = ChatOpenAI(temperature=0, model=\"gpt-3.5-turbo\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e64e9a5",
   "metadata": {},
   "source": [
    "다음의 방식으로 토큰 사용량을 확인할 수 있습니다.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f087fd3f",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.callbacks import get_openai_callback\n",
    "\n",
    "with get_openai_callback() as cb:\n",
    "    result = model.invoke(\"대한민국의 수도는 어디인가요?\")\n",
    "print(cb)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00f6cd40",
   "metadata": {},
   "source": [
    "HuggingFace 에 업로드 되어 있는 오픈소스 모델을 손쉽게 다운로드 받아 사용할 수 있습니다.\n",
    "\n",
    "아래의 리더보드에서 날마다 성능을 개선하는 오픈소스 리더보드를 확인할 수 있습니다.\n",
    "\n",
    "- [HuggingFace LLM Leaderboard](https://huggingface.co/spaces/HuggingFaceH4/open_llm_leaderboard)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ba46d84d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# HuggingFaceHub 객체 생성\n",
    "from langchain.llms import HuggingFaceHub\n",
    "\n",
    "repo_id = \"google/flan-t5-xxl\"\n",
    "\n",
    "t5_model = HuggingFaceHub(\n",
    "    repo_id=repo_id, model_kwargs={\"temperature\": 0.1, \"max_length\": 512}\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "628f91f5",
   "metadata": {},
   "outputs": [],
   "source": [
    "t5_model.invoke(\"Where is the capital of South Korea?\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d74046f",
   "metadata": {},
   "source": [
    "## RAG 템플릿 실험\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "88c64211",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 1: 문서 로드(Load Documents)\n",
    "# 문서를 로드하고, 청크로 나누고, 인덱싱합니다.\n",
    "from langchain.document_loaders import PyPDFLoader\n",
    "\n",
    "# PDF 파일 로드. 파일의 경로 입력\n",
    "file_path = \"data/SPRI_AI_Brief_2023년12월호_F.pdf\"\n",
    "loader = PyPDFLoader(file_path=file_path)\n",
    "\n",
    "# 단계 2: 문서 분할(Split Documents)\n",
    "text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=50)\n",
    "\n",
    "split_docs = loader.load_and_split(text_splitter=text_splitter)\n",
    "\n",
    "# 단계 3, 4: 임베딩 & 벡터스토어 생성(Create Vectorstore)\n",
    "# 벡터스토어를 생성합니다.\n",
    "vectorstore = FAISS.from_documents(documents=splits, embedding=OpenAIEmbeddings())\n",
    "\n",
    "# 단계 5: 리트리버 생성(Create Retriever)\n",
    "# 사용자의 질문(query) 에 부합하는 문서를 검색합니다.\n",
    "\n",
    "# 유사도 높은 K 개의 문서를 검색합니다.\n",
    "k = 3\n",
    "\n",
    "# (Sparse) bm25 retriever and (Dense) faiss retriever 를 초기화 합니다.\n",
    "bm25_retriever = BM25Retriever.from_documents(split_docs)\n",
    "bm25_retriever.k = k\n",
    "\n",
    "faiss_vectorstore = FAISS.from_documents(split_docs, OpenAIEmbeddings())\n",
    "faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={\"k\": k})\n",
    "\n",
    "# initialize the ensemble retriever\n",
    "ensemble_retriever = EnsembleRetriever(\n",
    "    retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5]\n",
    ")\n",
    "\n",
    "# 단계 6: 프롬프트 생성(Create Prompt)\n",
    "# 프롬프트를 생성합니다.\n",
    "prompt = hub.pull(\"rlm/rag-prompt\")\n",
    "\n",
    "# 단계 7: 언어모델 생성(Create LLM)\n",
    "# 모델(LLM) 을 생성합니다.\n",
    "llm = ChatOpenAI(model_name=\"gpt-3.5-turbo\", temperature=0)\n",
    "\n",
    "\n",
    "def format_docs(docs):\n",
    "    # 검색한 문서 결과를 하나의 문단으로 합쳐줍니다.\n",
    "    return \"\\n\\n\".join(doc.page_content for doc in docs)\n",
    "\n",
    "\n",
    "# 단계 8: 체인 생성(Create Chain)\n",
    "rag_chain = (\n",
    "    {\"context\": ensemble_retriever | format_docs, \"question\": RunnablePassthrough()}\n",
    "    | prompt\n",
    "    | llm\n",
    "    | StrOutputParser()\n",
    ")\n",
    "\n",
    "# 결과 출력\n",
    "print(f\"PDF Path: {file_path}\")\n",
    "print(f\"문서의 수: {len(docs)}\")\n",
    "print(\"===\" * 20)\n",
    "print(f\"[HUMAN]\\n{question}\\n\")\n",
    "print(f\"[AI]\\n{response}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e349c363",
   "metadata": {},
   "source": [
    "> 문서: data/SPRI_AI_Brief_2023년12월호\\_F.pdf (페이지 10)\n",
    "\n",
    "- LangSmith: https://smith.langchain.com/public/4449e744-f0a0-42d2-a3df-855bd7f41652/r\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "218d2974",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 8: 체인 실행(Run Chain)\n",
    "# 문서에 대한 질의를 입력하고, 답변을 출력합니다.\n",
    "question = \"삼성 가우스에 대해 설명해주세요\"\n",
    "response = rag_chain.invoke(question)\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "07b9b0df",
   "metadata": {},
   "source": [
    "> 문서: data/SPRI_AI_Brief_2023년12월호\\_F.pdf (페이지 12)\n",
    "\n",
    "- LangSmith: https://smith.langchain.com/public/2b2913c9-6b9c-4a19-bb16-dc2256e2fdbf/r\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "05d3d293",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 8: 체인 실행(Run Chain)\n",
    "# 문서에 대한 질의를 입력하고, 답변을 출력합니다.\n",
    "question = \"미래의 AI 소프트웨어 매출 전망은 어떻게 되나요?\"\n",
    "response = rag_chain.invoke(question)\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0b388974",
   "metadata": {},
   "source": [
    "> 문서: data/SPRI_AI_Brief_2023년12월호\\_F.pdf (페이지 14)\n",
    "\n",
    "- LangSmith: https://smith.langchain.com/public/17ef6df2-b012-4f8e-b0a8-62894d82c097/r\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "265b7d95",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 단계 8: 체인 실행(Run Chain)\n",
    "# 문서에 대한 질의를 입력하고, 답변을 출력합니다.\n",
    "question = \"YouTube 가 2024년에 의무화 한 것은 무엇인가요?\"\n",
    "response = rag_chain.invoke(question)\n",
    "print(response)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-test",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
